import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.Vector;

import com.sun.org.apache.bcel.internal.generic.NEWARRAY;


public class GeradorUtil {

	private static GeradorUtil instance;
	private int registradorAtual;
	private Stack<Integer> registradoresLivres;
	private Map<String, String> metodosNaoTerminais;
	public String nomeArquivo;
	private int labelAtual;
	private ParseNode noAtual;
	private HashMap<String, ArrayList<String>> variaveisComValor = new HashMap<String, ArrayList<String>>();
	
	private GeradorUtil() {
		registradorAtual = 0;
		labelAtual = 0;
		registradoresLivres = new Stack<Integer>();
		metodosNaoTerminais = new HashMap<String, String>();
		metodosNaoTerminais.put("expr_stmt", "trataExprStmt");
		metodosNaoTerminais.put("while_stmt", "trataWhileStmt");
		metodosNaoTerminais.put("func_def", "trataFuncDef");
		metodosNaoTerminais.put("call", "trataCall");
		metodosNaoTerminais.put("while_stmt", "trataWhile");
		metodosNaoTerminais.put("if_stmt", "trataIf");
		metodosNaoTerminais.put("if_stmt_loop", "trataElif");
		metodosNaoTerminais.put("for_stmt", "trataFor");

	}
	
	public int getRegistradorLivre() {
		if (!registradoresLivres.isEmpty()) {
			return registradoresLivres.pop();
		}
		int reg = registradorAtual;
		registradorAtual++;
		return reg;
	}
	
	public void liberaRegistrador(int n) {
		registradoresLivres.add(n);
	}
	
	public int getLabelLivre() {
		int label = labelAtual;
		labelAtual++;
		return label;
	}
	
	public static GeradorUtil getInstance() {
		if (instance == null) {
			instance = new GeradorUtil();
		}
		return instance;
	}
	
	private Map<String, String> getMapaParametros(List<String> tokens) {
		Map<String,String> mapaParametros = new HashMap<String, String>();
		for(int i = 0; i < tokens.size(); i++) {
			if(!tokens.get(i).equals(",") && !tokens.get(i).equals("(") && !tokens.get(i).equals(")")) {
				try {
					Integer.parseInt(tokens.get(i));
					mapaParametros.put(tokens.get(i), tokens.get(i));
				}
				
				catch (Exception e) {
					mapaParametros.put("R" + getRegistradorLivre(), tokens.get(i));
				}
			}
		}
		
		return mapaParametros;
	}
	
	public void trataCall(ParseNode no) throws IOException {
		
		
		Map<String, String> mapaParametros = getMapaParametros(this.getTokens(no).subList(1, this.getTokens(no).size()));
		
		for(String chave: mapaParametros.keySet()) {
			if(chave.startsWith("R")) {
				FileUtils.escreveNoArquivo("LD " + chave + ", " + mapaParametros.get(chave),nomeArquivo);
			}
		}
		
		String saidaInvoke = "INVOKE " + this.getTokens(no).get(0);
		
		for(String chave: mapaParametros.keySet()) {
			saidaInvoke += ", " + chave;			
		}
		
		FileUtils.escreveNoArquivo(saidaInvoke, nomeArquivo);
	}

	public void trataFor(ParseNode no) throws IOException{
		ArrayList<String> list = new ArrayList<String>();
		ArrayList<String> tokensFor = getTokens(no);
		ArrayList<String> relOp = new ArrayList<String>();
		try {
			temFilhoDois(no, "listmaker");
			ArrayList<String> tokens = getTokens(noAtual);
			
			for (int i = 0; i < tokens.size(); i++) {
				if(!tokens.get(i).equals(",")){
					list.add(tokens.get(i));
				}
			}
		} catch (Exception e) {
			list = variaveisComValor.get(tokensFor.get(3));
			
		}
		Integer r = getRegistradorLivre();
		
		relOp.add(tokensFor.get(1));
		relOp.add("<");
		relOp.add(list.size()+"");
		
		ArrayList<String> op = (ArrayList<String>) escreveListaOpRelacional(relOp.get(0), relOp.get(1), relOp.get(2), "while");
		for (int i = 0; i < op.size()-2; i++) {
			FileUtils.escreveNoArquivo(op.get(i), nomeArquivo);
		}

		FileUtils.escreveNoArquivo("LD R"+r+", *offset", nomeArquivo);
		FileUtils.escreveNoArquivo("ADD R"+r+", *offset, #1", nomeArquivo);
		FileUtils.escreveNoArquivo("ST "+tokensFor.get(1)+", R" + r, nomeArquivo);
		
		ParseNode proximoNo = getProximoNoChave(no);
		chamaProximoMetodo(proximoNo);
		
		FileUtils.escreveNoArquivo(op.get(op.size()-2), nomeArquivo);
		FileUtils.escreveNoArquivo(op.get(op.size()-1), nomeArquivo);
		
		
	}
	
	public void trataWhile(ParseNode no) throws IOException{
		ArrayList<String> tokens = getTokens(no);
		ArrayList<String> relOp = new ArrayList<String>(); 
		for (int i = 1; i < tokens.size(); i++) {
			if(tokens.get(i).equals(":"))
				break;
			relOp.add(tokens.get(i));
		}
		ArrayList<String> op = (ArrayList<String>) escreveListaOpRelacional(relOp.get(0), relOp.get(1), relOp.get(2), "while");
		for (int i = 0; i < op.size()-2; i++) {
			FileUtils.escreveNoArquivo(op.get(i), nomeArquivo);
		}
		ParseNode proximoNo = getProximoNoChave(no);
		chamaProximoMetodo(proximoNo);
		
		FileUtils.escreveNoArquivo(op.get(op.size()-2), nomeArquivo);
		FileUtils.escreveNoArquivo(op.get(op.size()-1), nomeArquivo);
	}
	
	public void trataIf(ParseNode no) throws IOException{
		ArrayList<String> tokens = getTokens(no);
		ArrayList<String> relOp = new ArrayList<String>(); 
		for (int i = 1; i < tokens.size(); i++) {
			if(tokens.get(i).equals(":"))
				break;
			relOp.add(tokens.get(i));
		}
		ArrayList<String> op = (ArrayList<String>) escreveListaOpRelacional(relOp.get(0), relOp.get(1), relOp.get(2), "if");
		for (int i = 0; i < op.size()-1; i++) {
			FileUtils.escreveNoArquivo(op.get(i), nomeArquivo);
		}
				
		ParseNode proximoNo = getProximoNoChave(no);
		chamaProximoMetodo(proximoNo);
		
		FileUtils.escreveNoArquivo(op.get(op.size()-1), nomeArquivo);
		
		proximoNo = getProximoElif(no);
		if(proximoNo.getChild("suite") != null){
			chamaProximoMetodo(proximoNo);
		}
		proximoNo = getProximoElse(no);
		if(proximoNo.getChild("suite") != null){
			chamaProximoMetodo(proximoNo);
		}
	}
	
	public void trataElif(ParseNode no) throws IOException{
		ArrayList<String> tokens = getTokens(no);
		ArrayList<String> relOp = new ArrayList<String>(); 
		for (int i = 1; i < tokens.size(); i++) {
			if(tokens.get(i).equals(":"))
				break;
			relOp.add(tokens.get(i));
		}
		
		ArrayList<String> op = (ArrayList<String>) escreveListaOpRelacional(relOp.get(0), relOp.get(1), relOp.get(2), "elif");
		for (int i = 0; i < op.size()-1; i++) {
			FileUtils.escreveNoArquivo(op.get(i), nomeArquivo);
		}
		
		ParseNode proximoNo = getProximoNoChave(no);
		chamaProximoMetodo(proximoNo);
		
		FileUtils.escreveNoArquivo(op.get(op.size()-1), nomeArquivo);
	}
	
	public void trataElse(ParseNode no) throws IOException{
		ParseNode proximoNo = getProximoNoChave(no);
		chamaProximoMetodo(proximoNo);
	}
	
	
	public void trataFuncDef(ParseNode no) throws IOException{
		ArrayList<String> tokens = getTokens(no);
		ArrayList<String> params = new ArrayList<String>();
		String nome = tokens.get(1);
		boolean inParams = false;
		for (Iterator<String> iterator = tokens.iterator(); iterator.hasNext();) {
			String token = (String) iterator.next();
			if(token.equals("(")){
				inParams = true;
				continue;
			}
			if(inParams){
				if(token.equals(")")){
					inParams = false;
					break;
				}
				if(!token.equals(","))
					params.add(token);
			}
		}
		
		String paramsString = "";
		for (int i = 0; i < params.size(); i++) {
			String string = params.get(i);
			if (i < params.size() - 1)
				paramsString += string + ":int,";
			else
				paramsString += string + ":int";
		}
		Integer r = getRegistradorLivre();
		FileUtils.escreveNoArquivo(nome + " proc " + paramsString, nomeArquivo);
		liberaRegistrador(r);
		ParseNode proximoNo = getProximoNoChave(no);
		chamaProximoMetodo(proximoNo);
		FileUtils.escreveNoArquivo("ret", nomeArquivo);
		FileUtils.escreveNoArquivo(nome+" endp", nomeArquivo);
	}
	
	public void trataExprStmt(ParseNode no) throws IOException {
		if(temFilho(no, "call") != null) {
			chamaProximoMetodo(temFilho(no, "call"));
		}
		
		ArrayList<String> tokens = this.getTokens(no);
		String saida = "";
		if(tokens.contains("=")){
			if(tokens.contains("[")){
				ArrayList<String> list = new ArrayList<String>();
				for (int i = 3; i < tokens.size()-1; i++) {
					if(!tokens.get(i).equals(",")){
						list.add(tokens.get(i));
					}
				}
				variaveisComValor.put(tokens.get(0), list);
				Integer r = getRegistradorLivre();
				saida += "LD R"+ r +", offset\n";
				saida += "ST " + tokens.get(0) + ", R" + r;
				FileUtils.escreveNoArquivo(saida, nomeArquivo);
				liberaRegistrador(r);
			}
			else{
				Integer r = getRegistradorLivre();
				Integer r2 = getRegistradorLivre();
				saida += operacaoAritmeticaParser(tokens.subList(2, tokens.size()), r, r2);
				saida += "ST " + tokens.get(0) + ", R" + r;
				FileUtils.escreveNoArquivo(saida, nomeArquivo);
				liberaRegistrador(r);
			}
		}
	}
	
	public String operacaoAritmeticaParser(List<String> tokens, Integer r, Integer r2) throws IOException {
		String saida = "";
		FileUtils.escreveNoArquivo("LD R" + r + ", " + Utils.getValue(tokens.get(0)), nomeArquivo);
		for(int i = 0; i < tokens.size(); i++) {
			if(tokens.get(i).equals("+")) {
				FileUtils.escreveNoArquivo("LD R" + r2 + ", " + Utils.getValue(tokens.get(i+1)), nomeArquivo);
				FileUtils.escreveNoArquivo("ADD R" + r + ", R" + r + ", R" + r2, nomeArquivo);
			}
			else if (tokens.get(i).equals("-")) {
				FileUtils.escreveNoArquivo("LD R" + r2 + ", " + Utils.getValue(tokens.get(i+1)), nomeArquivo);
				FileUtils.escreveNoArquivo("SUB R" + r + ", R" + r + ", R" + r2, nomeArquivo);
			}
			else if (tokens.get(i).equals("*")) {
				FileUtils.escreveNoArquivo("LD R" + r2 + ", " + Utils.getValue(tokens.get(i+1)), nomeArquivo);
				FileUtils.escreveNoArquivo("MUL R" + r + ", R" + r + ", R" + r2, nomeArquivo);
			}
			else if (tokens.get(i).equals("/")) {
				FileUtils.escreveNoArquivo("LD R" + r2 + ", " + Utils.getValue(tokens.get(i+1)), nomeArquivo);
				FileUtils.escreveNoArquivo("DIV R" + r + ", R" + r + ", R" + r2, nomeArquivo);
			}
			else if (tokens.get(i).equals("%")) {
				FileUtils.escreveNoArquivo("ST R" + r + ", R" + r + " MOD " + Utils.getValue(tokens.get(i+1)),nomeArquivo);
			}
		}
		return saida;
	}
	

	public void geraCodigo(ParseNode raiz) {
		emOrdem(raiz);
	}
	private ArrayList<String> getTokens(ParseNode no) {
		ArrayList<String> tokens = new ArrayList<String>();
		return getTokensRec(no, tokens);
	}
	
	
	private ArrayList<String> getTokensRec(ParseNode no, ArrayList<String> tokens) {
		
		if(no.isToken()){
			if(no.getLexeme() != "epsilon"){
				tokens.add(no.getLexeme());
				//System.out.println(no.getLexeme());
			}
		}
		List<ParseNode> filhos = no.getChildren();
		Iterator<ParseNode> i = filhos.iterator();
		while (i.hasNext()){
			getTokensRec(i.next(), tokens);
		}
		return tokens;
	}
	
	private ParseNode getProximoElse(ParseNode no) {
		ParseNode noSuite = no.getChild("if_stmt_aux");
		if(no.isToken()){
			return null;
		}
		return getProximoNoChaveAux(noSuite);
	}
	
	private ParseNode getProximoElif(ParseNode no) {
		ParseNode noSuite = no.getChild("if_stmt_loop");
		if(no.isToken()){
			return null;
		}
		return getProximoNoChaveAux(noSuite);
	}
	
	private ParseNode getProximoNoChave(ParseNode no) {
		ParseNode noSuite = no.getChild("suite");
		if(no.isToken()){
			return null;
		}
		return getProximoNoChaveAux(noSuite);
	}
		
	private ParseNode getProximoNoChaveAux(ParseNode no) {
		if(no.isToken()){
			return null;
		}
		if(metodosNaoTerminais.keySet().contains(no.getName())){
			return no;
		}
		List<ParseNode> filhos = no.getChildren();
		Iterator<ParseNode> i = filhos.iterator();
		while (i.hasNext()){
			emOrdem(i.next());
		}
		return null;
	}
	
	private void chamaProximoMetodo(ParseNode proximoNo){
		try {
			if(proximoNo == null){
				return;
			}
			Method metodo = this.getClass().getMethod(metodosNaoTerminais.get(proximoNo.getName()), ParseNode.class);
			metodo.invoke(this, proximoNo);
		} catch (Exception e) {
			System.out.println("Erro ao chamar o metodo " + metodosNaoTerminais.get(proximoNo.getName()));
		}
	}

	private ParseNode temFilho(ParseNode no, String nome) {	
		if(no != null && no.getName().equals(nome)) {				
			return no;		
		}
		List<ParseNode> filhos = no.getChildren();
		Iterator<ParseNode> i = filhos.iterator();
		while (i.hasNext()){
			ParseNode node = i.next();			
			return temFilho(node, nome);
		}
		return null;
	}
	
	private void temFilhoDois(ParseNode no, String nome) {	
		if(no != null && no.getName().equals(nome)) {				
			noAtual = no;		
		}
		List<ParseNode> filhos = no.getChildren();
		Iterator<ParseNode> i = filhos.iterator();
		while (i.hasNext()){
			ParseNode node = i.next();			
			temFilhoDois(node, nome);
		}
	}
	

	private void emOrdem(ParseNode no) {
		if(metodosNaoTerminais.keySet().contains(no.getName())){
			chamaProximoMetodo(no);
			return;
		}
		List<ParseNode> filhos = no.getChildren();
		Iterator<ParseNode> i = filhos.iterator();
		while (i.hasNext()){
			emOrdem(i.next());
		}
	}

	public void setNomeArquivo(String nome) {
		this.nomeArquivo = nome;
	}
	
	public ArrayList<String> escreveListaOpRelacional(String exp1, String op, String exp2, String tipo) {
		ArrayList<String> saida = new ArrayList<String>();
		int reg1 = getRegistradorLivre();
		int reg2 = getRegistradorLivre();
		int lab0 = getLabelLivre(); //expressao
		int lab1 = getLabelLivre(); //codigo
		int lab2 = getLabelLivre(); //proxima instrucao
		saida.add("L" + lab0 + ": LD R" + reg1 + ", " + Utils.getValue(exp1));
		saida.add("LD R" + reg2 + ", " + Utils.getValue(exp2));
		
		if (op.equals("==")) {
			saida.add("SUB R" + reg1 + ", R" + reg1 + ", R" + reg2);
			saida.add("JZ L" + lab1 + ", R" +reg1);
			saida.add("ST R"+ reg1 + ", #0");
			saida.add("JMP L" + lab2);
			saida.add("L" + lab1 + ": ST R" + reg1 + ", #1");
			if (tipo.equals("while")) {
				saida.add("JMP L" + lab0);
			}
			saida.add("L" + lab2 + ": ");
		} else if (op.equals("!=")) {
			saida.add("SUB R" + reg1 + ", R" + reg1 + ", R" + reg2);
			saida.add("JNZ L" + lab1 + ", R" +reg1);
			saida.add("ST R"+ reg1 + ", #0");
			saida.add("JMP L" + lab2);
			saida.add("L" + lab1 + ": ST R" + reg1 + ", #1");
			if (tipo.equals("while")) {
				saida.add("JMP L" + lab0);
			}
			saida.add("L" + lab2 + ": ");
		} else if (op.equals(">=")) {
			saida.add("JGE L" + lab1 + ", R" +reg1 + ", R" + reg2);
			saida.add("ST R"+ reg1 + ", #0");
			saida.add("JMP L" + lab2);
			saida.add("L" + lab1 + ": ST R" + reg1 + ", #1");
			if (tipo.equals("while")) {
				saida.add("JMP L" + lab0);
			}
			saida.add("L" + lab2 + ": ");
		} else if (op.equals("<=")) {
			saida.add("JLE L" + lab1 + ", R" +reg1 + ", R" + reg2);
			saida.add("ST R"+ reg1 + ", #0");
			saida.add("JMP L" + lab2);
			saida.add("L" + lab1 + ": ST R" + reg1 + ", #1");
			if (tipo.equals("while")) {
				saida.add("JMP L" + lab0);
			}
			saida.add("L" + lab2 + ": ");
			
		} else if (op.equals("<")) {
			saida.add("JL L" + lab1 + ", R" +reg1 + ", R" + reg2);
			saida.add("ST R"+ reg1 + ", #0");
			saida.add("JMP L" + lab2);
			saida.add("L" + lab1 + ": ST R" + reg1 + ", #1");
			if (tipo.equals("while")) {
				saida.add("JMP L" + lab0);
			}
			saida.add("L" + lab2 + ": ");
		} else if (op.equals(">")) {
			saida.add("JG L" + lab1 + ", R" +reg1 + ", R" + reg2);
			saida.add("ST R"+ reg1 + ", #0");
			saida.add("JMP L" + lab2);
			saida.add("L" + lab1 + ": ST R" + reg1 + ", #1");
			if (tipo.equals("while")) {
				saida.add("JMP L" + lab0);
			}
			saida.add("L" + lab2 + ": ");
		}
		return saida;
	}
}
